2.2-面向对象编程
Create by fall on 08 Sep 2021 Recently revised in 12 Jan 2023
关键字
instanceof
通过该关键字可以判断是否是继承关系
Array instanceof Object // true
let person = function(){}
let no = new person()
no instanceof person//true
instanceOf 原理
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
new
在 JS 中,函数本身就是构造函数,可以直接通过 new 进行声明构造函数。
new
的作用实际上就是把 function 转换为一个对象
自己生成一个 new
function myNew(fun,...args){
let obj = {}
const res = fun.call(obj,...args)
obj.__proto__ = fun.prototype
// 如果存在返回值
if(res){
// operate
}
return obj
}
function Person(name){
this.name = name
this.showName= ()=>{
console.log(name);
}
}
Person.prototype.say =function(){
console.log(this.name+'说:我没钱了');
}
const liu = myNew(Person,'狗剩')
liu.showName()
liu.say()
ES6 为 new
命令引入了一个 new.target
属性,这个属性一般用在构造函数中,返回 new
调用的那个构造函数。
如果构造函数不是通过new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,所以这个属性可以用来确定构造函数是怎么调用的,遇到箭头函数会抛错。
function fn(name) {
console.log('fn:',new.target)
}
fn('nanjiu') // undefined
new fn('nanjiu')
/*
fn: ƒ fn(name) {
console.log('fn:',new.target)
}
*/
let fn2 = (name) => {
console.log('fn2',new.target)
}
fn2('nan') // 报错 Uncaught SyntaxError: new.target expression is not allowed here
Class
ES6 实现的面向对象方式,Class 是 ES6 中创建类的语法糖
语法特性
可以用 class
关键字来定义一个类,类是对一类具有共同特征的事物的抽象,就比如可以把狗定义为一个类,狗有名字会叫也会跳;类是特殊的函数,就像函数定义的时候有函数声明和函数表达式一样,类的定义也有类声明和类表达式,不过类声明不同于函数声明,它是无法提升的;类也有 name
属性
// 类声明
class Dog {
constructor(name) { // 在创建对象时会构造一次
this.name = name
}
bark() {}
jump() {}
}
Dog.name // 'Dog'
// 类表达式:可以命名(类的 name 属性取类名),也可以不命名(类的 name 属性取变量名)
let Animal2 = class {
// xxx
}
Animal2.name // 'Animal2'
JS
中的类建立在原型的基础上(通过函数来模拟类,其实类就是构造函数的语法糖),和 ES5
中构造函数类似,但是也有区别,比如类的内部方法是不可被迭代的:
class Dog {
constructor() {}
bark() {}
jump() {}
}
Object.keys(Dog.prototype) // [] 类的内部方法是不可被迭代的
// 类似于
function Dog2(){}
Dog2.prototype = {
constructor() {},
bark() {},
jump() {},
}
Object.keys(Dog2.prototype) // ['constructor', 'bark', 'jump'] function可以被迭代
基于原型给类添加新方法
Object.assign(Dog.prototype, {
eat() {}
})
- 类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,
getter
和setter
都在严格模式下执行。 - 类内部的
this
默认指向类实例,所以如果直接调用原型方法或者静态方法会导致this
指向运行时的环境,而类内部是严格模式,所以此时的this
会是undefined
:
class Dog {
constructor(name) {
this.name = name
}
bark() {
console.log( `${this.name} is bark.` )
}
static jump() {
console.log( `${this.name} is jump.` )
}
}
let dog = new Dog('大黄')
let { bark } = dog
let { jump } = Dog
bark() // TypeError: Cannot read property 'name' of undefined
jump() // TypeError: Cannot read property 'name' of undefined
方法和关键字
constructor
方法是类的默认方法,通过 new
关键字生成实例的时候,会自动调用;constructor
默认会返回实例对象:
class Point {}
// 一个类必须有 constructor 方法,如果没有显示定义,则会自动添加一个空的,同下
class Point {
constructor() {}
}
通过 get
和 set
关键字拦截某个属性的读写操作:
class Dog {
get age(){
return 1
}
}
用 static
关键字给类定义静态方法,静态方法不会存在类的原型上,所以不能通过类实例调用,只能通过类名来调用,静态方法和原型方法可以同名:
class Dog {
bark() {}
jump() {
console.log('原型方法')
}
static jump() {
console.log('静态方法')
}
}
Object.getOwnPropertyNames(Dog.prototype) // ['constructor', 'bark', 'jump']
Dog.jump() // '静态方法'
let dog = new Dog()
dog.jump() // '原型方法'
new.target
属性允许你检测函数、构造方法或者类是否是通过 new
运算符被调用的。在通过 new
运算符被初始化的函数或构造方法中,new.target
返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target
的值是 undefined
,子类继承父类的时候会返回子类:
class Dog {
constructor() {
console.log(new.target.name)
}
}
function fn(){
if (!new.target) return 'new target is undefined'
console.log('fn is called by new')
}
let dog = new Dog() // 'Dog'
fn() // 'new target is undefined'
new fn() // 'fn is called by new'
类的继承
类可以通过 extends
关键字实现继承,如果子类显示的定义了 constructor
则必须在内部调用 super()
方法,内部的 this
指向当前子类:
class Animal {
constructor(name) {
this.name = name
}
run() {
console.log(`${this.name} is running.`)
}
}
class Dog extends Animal{
constructor(name){
super(name) // 必须调用,就相当于调用了 Animal 的 constructor
this.name = name
}
bark() {
console.log(`${this.name} is barking.`)
}
}
let dog = new Dog('大黄')
dog.run() // '大黄 is running.'
通过 super()
调用父类的构造函数或者通过 super
调用父类的原型方法;另外也可以在子类的静态方法里通过 super
调用父类的静态方法:
// 基于上面的代码改造
class Dog extends Animal{
constructor(name){
super(name) // 调用父类构造函数
this.name = name
}
bark() {
super.run() // 调用父类原型方法
console.log(`${this.name} is barking.`)
}
}
let dog = new Dog()
dog.bark()s
// '大黄 is running.'
// '大黄 is barking.'
子类的 __proto__
属性,表示构造函数的继承,总是指向父类;子类 prototype
属性的 __proto__
属性,表示方法的继承,总是指向父类的prototype属性:
class Animal {}
class Dog extends Animal {}
Dog.__proto__ === Animal // true
Dog.prototype.__proto__ === Animal.prototype // true
子类原型的原型指向父类的原型:
// 基于上面的代码
let animal = new Animal()
let dog = new Dog()
dog.__proto__.__proto__ === animal.__proto__ // true
使用 extends
还可以实现继承原生的构造函数,如下这些构造函数都可以被继承:
String()
、Number()
、Boolean()
、Array()
、Object()
、Function()
、Date()
、RegExp()
、Error()
class MyString extends String {
constructor(name){
super(name)
this.name = name
}
welcome() {
return `hello ${this.name}`
}
}
let ms = new MyString('布兰')
ms.welcome() // 'hello 布兰'
ms.length // 2
ms.indexOf('兰') // 1
静态字段
直接通过类来调用,这就称为“静态方法”。
ES 2022 支持静态字段
class MyString {
constructor(name){
this.name = name
}
// 通过在类中使用 static 关键字,来声明静态方法
static welcome() {
return `hello ${this.name}`
}
hello() {
return `hello ${this.name}`
}
}
let ms = new MyString('布兰')
ms.welcome() // 'hello 布兰'
私有字段
ES 2022 支持私有字段
静态公有字段和静态方法一样只能通过类名调用;私有属性和私有方法只能在类的内部调用,外部调用将报错:
class Dog {
age = 12 // 公有字段
static sex = 'male' // 静态公有字段
#secret = '我是人类的好朋友' // 私有字段
#getSecret() { // 私有方法
return this.#secret
}
}
Dog.sex // 'male'
let dog = new Dog()
dog.#getSecret() // SyntaxError
class SampleClass {
/*
instead of:
constructor() { this.publicID = 42; }
*/
publicID = 42; // public field
/*
instead of:
static get staticPublicField() { return -1 }
*/
static staticPublicField = -1;
// static private field
static #staticPrivateField = 'private';
//private methods
#privateMethod() {}
// static block
static {
// executed when the class is created
}
}
ergonomic brand checks for private fields
Brand checks without exceptions. 📕
class C {
#brand;
#method() {}
get #getter() {}
static isC(obj) {
// in keyword to check
return #brand in obj && #method in obj && #getter in obj;
}
}
公共和私有字段声明是 JavaScript 标准委员会 TC39 提出的实验性功能(第 3 阶段)。浏览器中的支持是有限的,但是可以通过 Babel 等系统构建后使用此功能。
装饰器
JS 的装饰器还在 Stage 3 draft 阶段,但是 ts 的已经可以用了,虽然可能还会再有改动
装饰器的 Polyfill 使用的是 defineProperty 我个人也不是很清楚 Proxy 和 defineProperty 的使用界限,或许会一方代替另一方。当然这都无关代码开发。
使用的时候要搞清楚一点,进行修饰的始终是个方法,用 @
后面跟的是个方法,因此也有一部分使用了高阶函数,想了解高阶函数,可以查看
类的装饰器
因为没有对应的装饰器环境,推迟更新,等待下次填坑(2022-01-27)
// 一般用法
// 装饰器一般是一个方法,用来对类进行装饰。该方法内的第一个参数为类
const Knowlage = (specc)=>{
specc.prototype.name = '411428'
specc.prototype.speak = function(err){
console.log(this.locate);
}
}
@Knowlage
class Student{
constructor(name){
this.name = name
}
locate="河北省"
speak(){
console.log('老子什么都不输出');
}
}
// 高阶函数用法
const Wisdom = (name, age) => {
console.log(age);
return function (specc) {
specc.prototype.test = '444';
specc.prototype.speak = function (err) {
console.log(this.locate);
}
}
}
@Wisdom('平淡', 66)
class Talent {
constructor(name) {
this.name = name;
}
locate = '河北省';
name;
speak() {
console.log('老子什么都不输出');
}
}
属性的装饰器
装饰器组合
其他的语言已经有装饰器的比较成熟的规范,JS正在进行跟进,哈哈哈哈哈,都拿来吧老子还想学(疯掉.jpg)。
decorator,用于装饰一个类,装饰就在于,你已经有一个非常大的对象了,而你想在其中几个对象中进行修改,但是再创建一个新的对象也不合适,所以就是用装饰器,对其中一部分对象进行修饰(小幅度的改动)。
类的修饰器
function language(value){
return function(target){
target.language = value
}
}
@language('English')
class Country{}
Country.language // English
实例方法修饰器
修饰函数有三个参数
- target 类的 prototype
- name 修饰的方法名称
- escriptor 该方法的修饰器
// 修饰函数有三个参数
JS的修饰器主要依赖于
Object.defineProperty
功能,当解释器遇到了@decorator
的语法时,就会调用这个修饰器函数对属性(方法)的描述符(descriptor)进行操作,然后将修饰过的descriptor
重新定义属性
ES5 面向对象
class 是 ES6 的语法糖,可以直接声明类,在没有语法糖之前如何实现呢?
对象的封装方式
普通封装(调用方法返回对象)
function creatPhone(name,prise){
var obj = new Object();
obj.name = name;
obj.prise = prise;
console.log(obj.name);
console.log(obj.prise);
return obj;
}
var xiaomi = creatPhone("小米","996");
var zhongxing = creatPhone("华为","007");
new 封装(通过方法创建对象)
构造函数(使用
new
关键字创建的对象)构造函数一般首字母大写
function Phone(name,prise){
this.name = name;
this.prise = prise;
this.showname=function(){
alert(obj.name);
};
alert(this.prise);
}
var xiaomi = new Phone("小米","996");
xiaomi.showname()
两者区别
-
普通封装
(方法)没办法继承,只是通过方法创建了个独立对象
执行方法,返回值是创建好的对象
-
new 封装
当前函数中的 this 指向新创建的对象
自动返回新创建的对象,可以封装函数可以继承
原型对象
原型通常指的是 prototype
和 __proto__
这两个原型对象。
prototype
(显式原型对象):prototype
上的内容会在 new
的时候,作为新对象的属性保留下来。
// 构造函数
function Phone(name, prise) {
this.name = name;
this.prise = prise;
};
// 构造函数的原型对象
Phone.prototype.showName = function () {
alert("选择生产的手机为:" + this.name);
}
Phone.prototype.showWork = function () {
alert("工作时间为" + this.prise);
}
var xiaomi = new Phone("小米", "996");
var huawei = new Phone("华为", "007");
xiaomi.showName();
huawei.showWork();
__proto__
(隐式原型对象):__proto__
上的内容会作为静态方法在构造函数上直接提供调用
给构造函数上添加原型对象 prototype,能为原型对象添加方法,那么构造函数构造出来的对象共享原型上所有的方法。
prototype
和 __proto__
// function 声明一个类,常用的内容
function Puppy(age) {
this.puppyAge = age; // this 指向创建后的对象,认为初始是空的即可
}
Puppy.prototype.say = function(){
console.log('旺旺');
}
Puppy.__proto__.say = function(){
console.log('阿嘎');
}
// 实例化时可以传年龄参数了
const myPuppy = new Puppy(2);
myPuppy.puppyAge // 证明了指向的是构造后的函数
myPuppy.say() // 调用的是 Puppy 使用 prototype 生成的
Puppy.say() // 调用的是 Puppy 使用 __proto__ 生成的
// console.log(new Puppy()==new Puppy()) // false
Puppy.prototype.say === myDog.say // true
myPuppy.__proto__ === Puppy.prototype // true
Puppy.prototype.__proto__ === Object.prototype // true 说明,所有 new 的构造方法 prototype 都是挂载在一个 new 的 Object 上面,如果要实现继承,只要更改 Puppy.prototype.__proto__ 让它指向父类的 prototype 即可
constructor
一个保留关键字,用来指向 prototype
或者 __proto__
链接的对象
function Puppy(age) {
this.puppyAge = age; // this 指向创建后的对象,认为初始是空的即可
}
Puppy.prototype.say = function(){
console.log('旺旺');
}
Puppy.__proto__.say = function(){
console.log('阿嘎');
}
const myPuppy = new Puppy(2);
myPuppy.__proto__.constructor.__proto__.say()
myPuppy.say()
如果修改构造函数的 constructor
并不会修改构造函数
或者直接理解,
prototype.constructor
只是一个指针,类似于 this 改变 this 只是改变 this 的指向,而不是 this 指向内容的值
function Puppy(age) {
this.puppyAge = age;
}
// 如果修改 Puppy.prototype.constructor 只会修改 constructor 指针的指向
Puppy.prototype.constructor = function myConstructor(age) {
this.puppyAge = age + 1;
console.log(this.puppyAge)
}
const myPuppy = new Puppy(5)
myPuppy2.constructor(999)
静态方法的实现
比较直接的实现
function Puppy(age){
this.puppyAge = age
}
// name 为保留字,不能设置 Puppy.name
Puppy.cuier = '高'
console.log(Puppy.cuier)
在 Puppy.__proto__
中修改,会连带修改 Puppy 上的内容
function Puppy(age) {
this.puppyAge = age
}
Puppy.cuier = '临高'
Puppy.__proto__.cuier = '启明'
console.log(Puppy.cuier)
console.log(Puppy.__proto__.cuier)
console.log(Puppy.__proto__.cuier === Puppy.cuier)
Puppy.__proto__.fake = '虚假'
console.log(Puppy.fake)
console.log(Puppy.__proto__.fake)
console.log(Puppy.__proto__.fake === Puppy.fake)
Puppy.truth = '真实'
console.log(Puppy.truth)
console.log(Puppy.__proto__.truth)
继承的实现
继承:子类继承父类,会让子类拥有所有父类对象上面的内容。
子类上可以添加内容,或者是覆盖父类上面的内容
如何实现继承,要同时实现原型的继承和静态方法的继承。
原型的继承,让 __proto__
指向父类原型的 prototype
就可以了
function Parent() {}
function Child() {}
Child.prototype.__proto__ = Parent.prototype
但是这样做,只是让 Child
能够访问到 Parent
的 prototype
所以,必须使用
function Parent() {
this.parentAge = 50
}
function Child() {}
// Child.prototype.__proto__ = Parent.prototype;// 如果通过该方法进行继承,只能让 Child 访问,Child 的子对象无法访问
Child.prototype.__proto__ = new Parent() // 通过让 Child 的原型指向 Parent 来实现继承
const obj = new Child()
console.log(obj.parentAge)
或者使用
function Parent() {
this.parentAge = 50
}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor = Child
// 如果 Child.prototype 指向了 Parent,Child.prototype.constrcutor 也会跟着指向 Parent,而不是 Child
const obj = new Child()
console.log(Parent.prototype.constructor)
静态方法的继承
如果将父对象的原型对象赋值给子对象,会导致两个对象公用同一个原型对象
解决办法,用 for in 循环进行原型对象的赋值
function Dog({name,sex,master}){
this.name = name;
this.sex=sex;
this.master =master;
}
Dog.prototype.file = function(){
alert(`这只狗是${this.master}先生家的一条狗,是一条${this.sex}${this.name}`);
};
function TinyDog({name,sex,master,weight}){
Dog.call(this,{
name:name,
sex:sex,
master:master
});
this.weight = weight;
};
// 使用for循环进行继承
for(var attr in Dog.prototype){
TinyDog.prototype[attr] = Dog.prototype[attr];
}
var bomei = new TinyDog({
name:"博美犬",
sex:"male",
master:"老刘",
weight:0.78
});
bomei.file();
var alasika = new TinyDog({
name:"阿拉斯加",
sex:"female",
master:"老赵",
weight:1.8
})
// 用构造函数构造的对象,有一个__proto__,指向构造出这个对象的构造函数原型
alert(bomei.__proto__==Dog.prototype);
// instanceof 用于判断某一个对象是否是这个构造函数构造的
alert(bomei instanceof Dog);
- 继承侧重的是父一级继承的构造函数和方法
- 多态侧重的是子一级可以重写构造函数和方法
其它继承方式
通过 for...in
循环遍历继承
for(var funcName in person.prototype){
Worker.prototype[funcName] = Person.prototype[funcName];
}
Object.create()
Worker.prototype = Object.create(Person.prototype);
// 使用 Worker 继承 Person
调用函数构造继承
Worker.prototype.__proto__ = new Person();
总结
prototype
本身也是对象,所以他也有 __proto__
,指向了他父级的 prototype
。__proto__
和 prototype
的这种链式指向构成了 JS 的原型链。原型链的最终指向是Object
的原型。Object
上面原型链是null
,即Object.prototype.__proto__ === null
。
prototype.constructor
指向的是构造函数,也就是类函数本身。改变这个指针并不能改变构造函数。
为了让实例化出来的对象能够访问到prototype
上的属性和方法,实例对象的__proto__
指向了类的prototype
。所以prototype
是函数的属性,不是对象的。对象拥有的是__proto__
,是用来查找prototype
的。
参考文章
相关文章
知识点 | 链接 |
---|---|
装饰器 | https://www.jianshu.com/p/afef44d449bd |
装饰器 | https://juejin.cn/post/6844903876605280269 |
装饰器 | https://juejin.cn/post/6844904100144889864 |
装饰器 | https://juejin.cn/post/6999451760934780941 |